using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace LazyMapper.Core
{
    public class MapperInstance : IMapper
    {
        private readonly IDictionary<int, ITypeMapping> _mappings = new Dictionary<int, ITypeMapping>();

        public MapperInstance()
        {
            Initialize();
        }

        #region IMapper Members

        public ITypeMappingSetup<TInput, TOutput> Create<TInput, TOutput>()
        {
            var typeMapping = new TypeMapping<TInput, TOutput>();
            if (_mappings.ContainsKey(typeMapping.Key))
                throw new Exception(string.Format("Adding duplicate [{0},{1}] mapping", typeof (TInput).Name,
                                                  typeof (TOutput).Name));
            _mappings.Add(typeMapping.Key, typeMapping);
            return typeMapping;
        }

        public TOutput Map<TInput, TOutput>(TInput input)
        {
            return Map(input, default(TOutput));
        }

        public TOutput Map<TInput, TOutput>(TInput input, TOutput output)
        {
            ITypeMapping<TInput, TOutput> mapper = FindMappingAndInitialize<TInput, TOutput>();
            output = (TOutput) mapper.Construct(input, output);
            return mapper.MapValues(input, output);
        }

        #endregion

        internal object Map(Type memberType, object input, Type memberTypes, object output)
        {
            ITypeMapping mapper = FindMappingAndInitialize(memberType, memberTypes);
            output = mapper.Construct(input, output);
            return mapper.MapValues(input, output);
        }

        public void Clear()
        {
            _mappings.Clear();
            Initialize();
        }

        private void Initialize()
        {
            MappingsAdd((ITypeMapping) new TypeMapping<Int64, string>().Constructor(Convert.ToString));
            MappingsAdd((ITypeMapping) new TypeMapping<Int32, string>().Constructor(Convert.ToString));
            MappingsAdd((ITypeMapping) new TypeMapping<Int16, string>().Constructor(Convert.ToString));
            MappingsAdd((ITypeMapping) new TypeMapping<double, string>().Constructor(Convert.ToString));
            MappingsAdd((ITypeMapping) new TypeMapping<bool, string>().Constructor(Convert.ToString));
        }

        private void MappingsAdd(ITypeMapping constructor)
        {
            _mappings.Add(constructor.Key, constructor);
        }

        #region Private Methods

        private ITypeMapping<TInput, TOutput> FindMappingAndInitialize<TInput, TOutput>()
        {
            return (ITypeMapping<TInput, TOutput>) FindMappingAndInitialize(typeof (TInput), typeof (TOutput));
        }

        private ITypeMapping FindMappingAndInitialize(Type inputMemberType, Type outputMemberType)
        {
            int hashCode = TypeMappingHelper.GetTypeHashCode(inputMemberType, outputMemberType);
            try
            {
                if (!_mappings.ContainsKey(hashCode))
                {
                    if (IsEnumerable(inputMemberType) && IsEnumerable(outputMemberType))
                    {
                        try
                        {
                            AutoCreateNewMapper(inputMemberType, outputMemberType);
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }
                    }
                }
                ITypeMapping typeMapping = _mappings[hashCode];
                typeMapping.Initialize(this);
                return typeMapping;
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Missing mapping [{0},{1}] mapping", inputMemberType.FullName,
                                                  outputMemberType.FullName),ex);
            }
        }

        private void AutoCreateNewMapper(Type inputMemberType, Type outputMemberType)
        {
            Type specificType = typeof (TypeMapping<,>).MakeGenericType(new[] {inputMemberType, outputMemberType});
            var instance = (ITypeMapping) Activator.CreateInstance(specificType, null);
            instance.BuildListMapper(this);
            _mappings.Add(instance.Key, instance);
        }

        private bool IsEnumerable(Type inputMemberType)
        {
            return inputMemberType.IsArray ||
                   inputMemberType.GetInterfaces().Where(
                       i =>
                       i == typeof (IEnumerable) ||
                       (i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IEnumerable<>))).Any();
        }

        #endregion
    }
}